package com.example.Service.Impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.Service.AccountService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.vo.request.*;
import com.example.mapper.AccountMapper;
import com.example.entity.dto.Account;
import com.example.utils.Const;
import com.example.utils.FlowUtils;
import jakarta.annotation.Resource;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 作者:xuSen
 * 日期2025/4/13 18:27
 */
@Service
public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> implements AccountService {
    @Resource
    private FlowUtils flowUtils;
    @Resource
    private AmqpTemplate amqpTemplate;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private PasswordEncoder passwordEncoder;

    /**
     * 重写loadUserByUsername方法以加载用户详细信息
     * 此方法根据用户名或电子邮件检索用户信息，并返回 UserDetails 对象
     * 如果找不到用户，将抛出 UsernameNotFoundException 异常
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 根据用户名或电子邮件查找账户
        Account account = this.FindByUsernameOrEmail(username);
        // 如果账户不存在，抛出异常
        if (account == null) {
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        // 构建并返回 UserDetails 对象
        return User
                .withUsername(username)
                .password(account.getPassword())
                .roles(account.getRole())
                .build();
    }

    /**
     * 重写注册邮箱验证码方法
     * 该方法用于生成并发送注册邮箱验证码，同时限制发送频率
     *
     * @param type  验证码类型，用于区分不同的验证场景
     * @param email 用户输入的邮箱地址
     * @param ip    用户请求的IP地址，用于频率限制
     * @return 如果发送成功，返回null；如果发送失败或请求频繁，返回相应的错误信息
     */
    @Override
    public String registerEmailVerifyCode(String type, String email, String ip) {
        // 使用IP地址作为锁对象，确保同一IP在同一时间只能有一个线程执行此方法
        synchronized (ip.intern()) {
            // 检查是否达到发送频率限制
            if (!this.VerifyLimit(ip)) {
                return "请求频繁,请稍后再试";
            }
            // 生成6位随机验证码
            Random random = new Random();
            int code = random.nextInt(899999) + 100000;
            // 创建包含验证码信息的Map对象
            Map<String, Object> date = Map.of("type", type, "email", email, "code", code);
            // 通过AMQP模板发送验证码信息到邮件队列
            amqpTemplate.convertAndSend("email", date);
            // 将验证码存储到Redis中，设置过期时间为3分钟
            stringRedisTemplate.opsForValue().set(Const.VERIFY_EMAIL_DATA + email, String.valueOf(code), 3, TimeUnit.MINUTES);
            // 验证码发送成功，返回null
            return null;
        }
    }

    /**
     * 注册邮件账户
     * <p>
     * 该方法用于注册一个新的邮件账户它接受一个包含用户信息的EmailRegisterVo对象作为参数，
     * 并在系统中创建一个新的邮件账户如果注册成功，返回一个表示新账户的字符串标识；
     * 如果注册失败，则可能返回null或特定的错误信息
     *
     * @param emailRegisterVo 包含用户注册信息的对象，如用户名、密码和电子邮件地址
     * @return 如果注册成功，返回表示新账户的字符串标识；如果注册失败，返回null或错误信息
     */
    @Override
    public String registerEmailAccount(EmailRegisterVo emailRegisterVo) {
        // 提取用户输入的邮箱和用户名
        String email = emailRegisterVo.getEmail();
        String username = emailRegisterVo.getUsername();

        // 从Redis中获取验证邮箱的验证码
        String code = stringRedisTemplate.opsForValue().get(Const.VERIFY_EMAIL_DATA + email);
        // 如果Redis中没有该邮箱的验证码，提示用户获取验证码
        if (code == null) return "请获取验证码";
        // 如果用户输入的验证码与Redis中的验证码不匹配，提示验证码错误
        if (!code.equals(emailRegisterVo.getCode())) return "验证码有错";
        // 检查邮箱是否已被注册
        if (this.existsAccountByEmail(email)) return "邮箱已被注册，请更换";
        // 检查用户名是否已被注册
        if (this.existsAccountByUsername(username)) return "用户名已被注册，请更换";
        // 对用户输入的密码进行加密处理
        String password = passwordEncoder.encode(emailRegisterVo.getPassword());
        // 创建一个新的账户对象
        Account user = new Account(null, username, password, email, "user", null,new Date());
        // 尝试保存新账户信息到数据库
        if (this.save(user)) {
            // 如果保存成功，删除Redis中的验证码信息，并返回null表示注册成功
            stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email);
            return null;
        } else {
            // 如果保存失败，返回错误信息
            return "注册失败，请联系管理苑";
        }
    }

    /**
     * 重置确认方法
     * 该方法用于处理重置过程中的确认操作，具体逻辑取决于实现
     *
     * @param confirmResetVO 包含重置确认所需信息的视图对象
     * @return 返回重置确认的结果，具体格式和内容取决于实现
     */
    @Override
    public String resetConfirm(ConfirmResetVO confirmResetVO) {
        // 获取用户输入的邮箱
        String email = confirmResetVO.getEmail();
        // 从Redis中获取该邮箱对应的验证码
        String code = stringRedisTemplate.opsForValue().get(Const.VERIFY_EMAIL_DATA + email);
        // 如果验证码为空，则提示用户获取验证码
        if (code == null) {
            return "请获取验证码";
        }
        // 如果用户输入的验证码与Redis中存储的验证码不一致，则提示用户验证码错误
        if (!code.equals(confirmResetVO.getCode())) {
            return "验证码有错,请重新输入";
        }
        // 如果验证码验证成功，则返回null，表示验证通过
        return null;
    }

    /**
     * 重置邮箱账户密码
     * 该方法用于处理通过邮箱重置密码的请求，具体逻辑包括验证邮箱的有效性、生成新密码等
     * @param emailResetVO 包含邮箱注册信息的视图对象，用于邮箱验证和密码重置
     * @return 返回重置密码的结果，通常包括成功或失败的提示信息
     */
    @Override
    public String resetEmailAccountPassword(EmailResetVO emailResetVO) {
        // 提取邮箱视图对象中的邮箱和验证码信息
        String email = emailResetVO.getEmail();
        String code = emailResetVO.getCode();
        // 使用密码编码器对新密码进行加密处理，确保数据安全性
        String password = passwordEncoder.encode(emailResetVO.getPassword());
        // 创建确认重置视图对象，用于后续的验证流程
        ConfirmResetVO confirmResetVO = new ConfirmResetVO(email, code);
        // 调用重置确认方法，验证用户提交的信息是否合法
        String verify = this.resetConfirm(confirmResetVO);
        // 如果验证结果不为空，说明验证失败，直接返回错误信息
        if (verify != null) {
            return verify;
        }
        // 更新数据库中的密码信息
        boolean update = this.update().eq("email", email).set("password", password).update();
        // 如果更新成功，删除Redis中缓存的邮箱验证信息
        if (update) {
//            stringRedisTemplate.delete(Const.VERIFY_EMAIL_DATA + email);
            this.deleteEmailVerifyCode(email);
        }
        // 返回null表示密码重置成功
        return null;
    }
    /**
     * 根据账户ID查找账户信息
     *
     * @param id 账户的唯一标识符
     * @return 返回Account对象，如果找不到则返回null
     */
    @Override
    public Account findAccountById(int id) {
        return this.getById(id);
    }


    /**
     * 修改用户邮箱
     *
     * @param id 用户ID
     * @param vo 包含新邮箱和验证码的视图对象
     * @return 如果修改失败返回错误信息，否则返回null
     */
    @Override
    public String modifyEmail(int id, ModifyEmailVO vo) {
        // 获取用户输入的新邮箱地址
        String email = vo.getEmail();
        // 尝试获取该邮箱的验证码
        String code = this.getEmailVerifyCode(email);
        // 如果验证码为空，提示用户先获取验证码
        if(code == null) return "请先获取验证码";
        // 如果验证码不匹配，提示用户验证码错误
        if(!code.equals(vo.getCode())) return "验证码错误，请重新输入";
        // 验证码验证通过后，删除验证码以增强安全性
        this.deleteEmailVerifyCode(email);
        // 检查新邮箱是否已被其他用户使用
        Account account = this.FindByUsernameOrEmail(email);
        // 如果找到匹配的账户且不是当前用户，提示用户邮箱已被使用
        if(account != null && account.getId() != id) return "该邮件地址已被其他用户使用，请重新更换";
        // 更新用户邮箱
        this.update().eq("id", id).set("email", email).update();
        // 所有检查通过，返回null表示修改成功
        return null;
    }
    /**
     * 移除Redis中存储的邮件验证码
     * @param email 电邮
     */
    private void deleteEmailVerifyCode(String email){
        String key = Const.VERIFY_EMAIL_DATA + email;
        stringRedisTemplate.delete(key);
    }
    /**
     * 重写修改密码方法
     *
     * @param id 用户ID，用于定位需要修改密码的用户
     * @param vo 包含旧密码和新密码的视图对象，用于验证旧密码和设置新密码
     * @return 返回修改结果信息如果修改成功，返回null；如果旧密码错误或更新失败，返回相应的错误信息
     */
    @Override
    public String changePassword(int id, ChangPasswordVO vo) {
        // 查询用户当前的密码
        String password = this.query().eq("id", id).one().getPassword();
        // 验证旧密码是否正确
        if(!passwordEncoder.matches(vo.getPassword(), password))
            return "旧密码错误，请重新输入";
        // 更新用户密码
        boolean success = this.update()
                .eq("id", id)
                .set("password", passwordEncoder.encode(vo.getNew_password()))
                .update();
        // 根据更新结果返回相应信息
        return success ? null : "未知错误，请稍后再试";
    }

    /**
     * 获取Redis中存储的邮件验证码
     * @param email 电邮
     * @return 验证码
     */
    private String getEmailVerifyCode(String email){
        String key = Const.VERIFY_EMAIL_DATA + email;
        return stringRedisTemplate.opsForValue().get(key);
    }
    /**
     * 检查指定邮箱是否存在
     *
     * @param email 邮箱，用于查询账户是否存在
     * @return 如果存在指定邮箱的账户，则返回true；否则返回false
     */
    private boolean existsAccountByEmail(String email) {
        return this.baseMapper.exists(new QueryWrapper<Account>().eq("email", email));
    }

    /**
     * 检查指定用户名的账户是否存在
     *
     * @param username 用户名，用于查询账户是否存在
     * @return 如果存在指定用户名的账户，则返回true；否则返回false
     */
    private boolean existsAccountByUsername(String username) {
        // 使用MyBatis-Plus的QueryWrapper构建查询条件，检查用户名是否已存在
        return this.baseMapper.exists(new QueryWrapper<Account>().eq("username", username));
    }


    /**
     * 验证验证邮件的发送限制
     * 该方法用于检查给定IP地址在过去一分钟内是否已经发送了验证邮件
     * 主要目的是防止某个IP地址频繁发送验证邮件，以避免潜在的滥用或攻击行为
     *
     * @param ip 发送验证邮件的IP地址
     * @return 如果在过去一分钟内该IP地址发送验证邮件的次数未超过限制，则返回true；否则返回false
     */
    private boolean VerifyLimit(String ip) {
        // 构造缓存键，用于标识特定IP地址的验证邮件发送限制
        String key = Const.VERIFY_EMAIL_LIMIT + ip;
        // 调用限流工具类的方法，检查该IP地址在接下来的60秒内是否可以发送验证邮件
        return flowUtils.limitOnceCheck(key, 60);
    }

    /**
     * 根据用户名或邮箱查询账户
     * 该方法旨在通过用户名或邮箱地址来查找用户账户信息它首先尝试通过用户名进行匹配，
     * 如果没有找到，则尝试通过邮箱地址进行匹配这种设计允许用户使用他们觉得最方便的方式进行登录
     */
    public Account FindByUsernameOrEmail(String username) {
        return this.query()
                .eq("username", username).or()
                .eq("email", username)
                .one();
    }



}
